package pseudoAssemblerCompiler.twopassassembler.model.assemblerAlgorithms;

import org.apache.commons.lang.StringUtils;
import pseudoAssemblerCompiler.twopassassembler.controller.Constants;
import pseudoAssemblerCompiler.twopassassembler.exceptions.RequiredDataNotSetException;
import pseudoAssemblerCompiler.twopassassembler.model.Utils;
import pseudoAssemblerCompiler.twopassassembler.model.assemblerAlgorithms.AbstractAssembler;
import pseudoAssemblerCompiler.twopassassembler.model.codeoperationtable.ICodeOperationTableModel;
import pseudoAssemblerCompiler.twopassassembler.model.entities.OperationCode;
import pseudoAssemblerCompiler.twopassassembler.model.entities.ReferenceTableEntity;
import pseudoAssemblerCompiler.twopassassembler.model.referencetable.IReferenceTableModel;
import pseudoAssemblerCompiler.twopassassembler.model.symbolnametable.ISymbolicNamesTableModel;

import java.util.List;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Основная логика работы ДвухПроходного Ассемблера №1
 *
 * @author Чекашов Р. И.
 */
public class FirstAssembler extends AbstractAssembler {

    private String _consoleMessage;

    public FirstAssembler(ICodeOperationTableModel codeOperationTable, ISymbolicNamesTableModel symbolicNamesTable, IReferenceTableModel referenceTable) {
        super(codeOperationTable, symbolicNamesTable, referenceTable);
    }

    @Override
    public String getConsoleMessage() {
        return _consoleMessage;
    }

    public void setConsoleMessage(String consoleMessage) {
        this._consoleMessage = consoleMessage;
    }

    /**
     * Обработка первой строки которая ищет директиву СТАРТ и устанавливает адрес загрузки
     * @param scanner
     * @return
     * @throws RequiredDataNotSetException
     */
    private int lookForStart(Scanner scanner) throws RequiredDataNotSetException {
        _isFirstPass = true;
        clearBuffers();
        clearCounters();
        List<String> param;
        int loadingAddr = 0;

        while (scanner.hasNextLine()) {
            String currentLine = scanner.nextLine();
            _programmCodelength += currentLine.length() + System.getProperty("line.separator").length();
            System.out.println(_programmCodelength +" = "+ currentLine.length() + " + "+ System.getProperty("line.separator").length());
            param = Utils.getTokens(currentLine);
            int amountOfToken = param.size();
            lineCount++;
            if(0 == amountOfToken) { //Пропускаем пустые строки
                continue;
            }

            if(amountOfToken != 3 || !("START".equalsIgnoreCase(param.get(1)))) {
                String msg = "Программа должна начинаться с [Название программы " +
                        "start адрес загрузки в шестнадцатиричном формате]";
                length = currentLine.length() + System.getProperty("line.separator").length();
                start = _programmCodelength - length;
                length = currentLine.length();
                notifyPublishers(msg, Constants.OperationCodeController.ERROR);
                throw new RequiredDataNotSetException(msg);
            }

            try {
                loadingAddr = Integer.decode("0x" + param.get(2));
            } catch (NumberFormatException ex) {
                showError(param.get(2), currentLine);
                String msg = String.format("Адрес загрузки не является 16-ным числом %s", param.get(2));
                notifyPublishers(msg, Constants.OperationCodeController.ERROR);
                throw new RequiredDataNotSetException(msg);
            }
            if (loadingAddr <= 0 || loadingAddr >= 16777215) {
                showError(param.get(2), currentLine);
                String msg = String.format("Адрес загрузки [%s] не может быть: нулевым," +
                        " отрицательным и большим 0xFFFFFF", param.get(2));
                notifyPublishers(msg, Constants.OperationCodeController.ERROR);
                throw new RequiredDataNotSetException(msg);
            }
            ReferenceTableEntity entity = new ReferenceTableEntity(param.get(0),
                    param.get(1), addrBuild(loadingAddr, 6), lineCount, _programmCodelength, currentLine);
            _referenceTable.add(entity);
            break;
        }

        return loadingAddr;
    }


    boolean isEndOfProgram;
    int loadingAddr = 0; //адрес загрузки

    @Override
    public void firstPass(String program) {
        _programmCodelength = 0;
        _referenceTable.reset();
        _symbolicNamesTable.reset();
        isEndOfProgram = false;
        _hasFirstPassErrors = false;
        int codeSize; //размер команды
        Scanner scanner = new Scanner(program);
        _addrCounter = 0;
        String[] param = new String[3];

        _addrBuilder = new StringBuilder(6);
        int[] arrTKO;
        OperationCode codeTKO;

        try {
            loadingAddr = lookForStart(scanner);
        } catch (RequiredDataNotSetException e) {
            return;
        }

        //Записываем в счетчик адресов адрес загрузки
        _addrCounter = loadingAddr;

        while (scanner.hasNextLine() && !isEndOfProgram) {
            String currentLine = scanner.nextLine();
            _programmCodelength += currentLine.length() + System.getProperty("line.separator").length();
            System.out.println(_programmCodelength +" = "+ currentLine.length() + " + "+ System.getProperty("line.separator").length());
            List<String> arrList = Utils.getTokens(currentLine);
            param = arrList.toArray(new String[arrList.size()]);
            int paramLength = param.length;
            lineCount++;
            if(0 == paramLength || StringUtils.isEmpty(param[0])) { //Пропускаем пустые строки
                continue;
            }

            try {
                if(paramLength == 2) {
                    twoTokenLineParser(param, currentLine);
                } else if(paramLength == 3) {
                    threeTokenLineParser(param, currentLine);
                    twoTokenLineParser(new String[]{param[1], param[2]}, currentLine);
                } else {
                    length = currentLine.length() + System.getProperty("line.separator").length();
                    start = _programmCodelength - length;
                    length = currentLine.length();
                    String msg = String.format("Строка [%s] некорректна", currentLine);
                    notifyPublishers(msg, Constants.OperationCodeController.ERROR);
                    return;
                }
            } catch (RequiredDataNotSetException e) {
                return;
            }
        }
    }

    private void threeTokenLineParser(String[] param, String currentLine) {
        //метка
        if (!param[0].isEmpty()) {
            if (_symbolicNamesTable.getSymbolNames().containsKey(param[0])) {
                showError(param[0], currentLine);
                String msg = String.format("Повторное определение метки [%s].", param[0]);
                notifyPublishers(msg, Constants.OperationCodeController.ERROR);
            } else { //помещаем метку в таблицу ТСИ
                _symbolicNamesTable.getSymbolNames().put(param[0], addrBuild(_addrCounter, 6));
                param[0] = addrBuild(_addrCounter, 6);
            }
        } else {
            param[0] = addrBuild(_addrCounter, 6);
        }
    }

    private void twoTokenLineParser(String[] param, String currentLine) throws RequiredDataNotSetException {
        OperationCode codeTKO;
        String[] addressCodeOperand = new String[3];
        //мнемокод операции
        if (_codeOperationTable.getCodeOperationTableElements().containsKey(param[0])) {
            codeTKO = _codeOperationTable.getCodeOperationTableElements().get(param[0]);
            if (codeTKO.is_directive()) { //встретилась директива
                if ("END".equalsIgnoreCase(param[0])) {
                    ReferenceTableEntity entity = new ReferenceTableEntity(addrBuild(_addrCounter, 6),
                            param[0], param[1], lineCount, _programmCodelength, currentLine);
                    _referenceTable.add(entity);
                    String startHexAddr = Integer.toHexString(loadingAddr);
                    if(!param[1].equals(startHexAddr)) {
                        showError(param[1], currentLine);
                        String msg = String.format("Неправильно указана точка входа в директиве END [%s] " +
                                "не совпадает с адресом загрузки[%s]!", param[1], startHexAddr);
                        notifyPublishers(msg, Constants.OperationCodeController.ERROR);
                        throw new RequiredDataNotSetException(msg);
                    }
                    if(!hasFirstPassErrors()) {
                        String msg = "Достигнут конец программы" + System.getProperty("line.separator")
                                + "Длина программы " + addrBuild(_addrCounter - loadingAddr, 0);
                        notifyPublishers(msg, Constants.OperationCodeController.INFO);
                    }
                    isEndOfProgram = true;
                    return;
                }
                manageDirectiv(param, codeTKO, currentLine);
                ReferenceTableEntity entity = new ReferenceTableEntity(addrBuild(_addrCounter, 6),
                        param[0], param[1], lineCount, _programmCodelength, currentLine);
                _referenceTable.add(entity);
                return;
            } else if (codeTKO.get_mnemonicCode() == null || codeTKO.get_binaryCode() == null ||
                    codeTKO.get_lengthOfCommand() == null || codeTKO.get_typeOfAddressing() == null) {
                String msg = "Незаданы параметры команды";
                notifyPublishers(msg, Constants.OperationCodeController.ERROR);
                throw new RequiredDataNotSetException(msg);
            }

            //встретилась мнемокоманда
            ReferenceTableEntity entity = new ReferenceTableEntity(addrBuild(_addrCounter, 6),
                    addrBuild(codeTKO.get_binaryCode() * codeTKO.get_lengthOfCommand() + codeTKO.get_typeOfAddressing(), 2),
                    param[1], lineCount, _programmCodelength, currentLine);
            _referenceTable.add(entity);
            //изменяем адрес
            _addrCounter += codeTKO.get_lengthOfCommand();

            //!TODO дописать
            //throw exception if not a number
        } else {
            showError(param[0], currentLine);
            String msg = String.format("Мнемокод операции [%s] в ТКО не найден", param[0]);
            notifyPublishers(msg, Constants.OperationCodeController.ERROR);
            throw new RequiredDataNotSetException(msg);
        }
        //записываем результат во вспомогательную таблицу
//        _referenceTable.getReferenceTableElements().add(addressCodeOperand);
    }

    @Override
    public void secondPass() {
        _isFirstPass = false;
        clearBuffers();
        List<ReferenceTableEntity> referenceTable = _referenceTable.getReferenceTableEntities();
        if (referenceTable == null || referenceTable.size() == 0) {
            String msg = "Вспомогательная таблица пуста referenceTable = " + referenceTable;
            notifyPublishers(msg, Constants.OperationCodeController.ERROR);
            throw new IllegalArgumentException(getConsoleMessage());
        }
        String lineOfOutputProgram = "H " + referenceTable.get(0).get_address() + " "
                + referenceTable.get(0).get_operand() + " " + referenceTable.get(referenceTable.size() - 1).get_address();
        _infoBuffer.append(lineOfOutputProgram);
        _infoBuffer.append(System.getProperty("line.separator"));

        String comandCode;
        String operand;
        int symbolCount = 0;

        StringBuilder oper = new StringBuilder();
        // начало формирования записей "T"
        for (int i = 1; i < referenceTable.size(); i++) {
            _infoBuffer.append("T " +referenceTable.get(i).get_address() + " ");
            String code = referenceTable.get(i).get_code();
            String refTableOperand = referenceTable.get(i).get_operand();
            lineCount = referenceTable.get(i).get_lineNumber();
            if (_codeOperationTable.getCodeOperationTableElements().containsKey(code)) {
                try {
                    codeOperationTableContainMnemonicCode(code, referenceTable.get(i));
                } catch (RequiredDataNotSetException e) {
                    if("end".equalsIgnoreCase(e.getMessage())) {
                        int infoBufferLength = _infoBuffer.length();
                        _infoBuffer.delete(infoBufferLength - 9, infoBufferLength);
                        _infoBuffer.append("E 000" + refTableOperand + System.getProperty("line.separator"));
                    } else {
                        showError(e.getMessage(), referenceTable.get(i));
                        String msg = String.format("Значение %s не является числом или оно слишком большое, или <= нулю!",
                                refTableOperand);
                        notifyPublishers(msg, Constants.OperationCodeController.ERROR);
                        LOGGER.error(msg);
                    }
                }
            } else {
                comandCode = code;
                operand = _symbolicNamesTable.getSymbolNames().get(refTableOperand);

                if ("R1,R2".equalsIgnoreCase(refTableOperand)) {
                    operand = "12";
                }

                if(operand == null) {
                    showError(refTableOperand, referenceTable.get(i));
                    String msg = String.format("В ТСИ символическое имя [%s] не существует", refTableOperand);
                    _hasSecondPassErrors = true;
                    notifyPublishers(msg, Constants.OperationCodeController.ERROR, referenceTable.get(i));
                    return;
                }

                symbolCount = comandCode.length() + operand.length();
                _infoBuffer.append(Utils.addrBuild(symbolCount, 2) + " " + comandCode + " " + operand + System.getProperty("line.separator"));
            }
        }
        _notificationCenter.postNotification(this, Constants.OperationCodeController.INFO);
    }

}